[add] GitHub actions of GitHub-reward & Lark-GitHub-bot#25
Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces new GitHub Actions workflows and Deno-based automation scripts to (1) distribute “reward” amounts when reward-labeled issues are closed, (2) generate monthly reward statistics, and (3) send GitHub event notifications to Lark/Feishu.
Changes:
- Add reward-claim and monthly reward-statistics workflows backed by new Deno scripts that tag releases / push tags.
- Add a Lark notification workflow and an event-to-card serialization script.
- Add a reward issue template and update recommended VS Code extensions.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| .vscode/extensions.json | Updates editor extension recommendations (adds Copilot Chat). |
| .github/workflows/statistic-member-reward.yml | Monthly scheduled workflow to compute reward summaries and publish a release/tag. |
| .github/workflows/Lark-notification.yml | Multi-event workflow to serialize GitHub event payloads and send to Lark. |
| .github/workflows/claim-issue-reward.yml | Workflow to calculate/split rewards on issue close and persist results via tags/comments. |
| .github/scripts/type.ts | Defines the Reward data shape used by reward scripts. |
| .github/scripts/transform-message.ts | Converts GitHub event payloads into a Lark interactive card JSON. |
| .github/scripts/share-reward.ts | Computes reward distribution from linked merged PR participants and persists it. |
| .github/scripts/deno.json | Adds Deno config (currently not auto-applied from repo root). |
| .github/scripts/count-reward.ts | Aggregates reward tags into a monthly summary and publishes tag/release. |
| .github/ISSUE_TEMPLATE/reward-task.yml | Adds an issue template for creating reward-labeled tasks. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| let rawYAML = ''; | ||
|
|
||
| for (const tag of rewardTags) rawYAML += (await $`git tag -l --format="%(contents)" ${tag}`) + '\n'; |
There was a problem hiding this comment.
In the loop building rawYAML, you’re concatenating the $ command result object directly into a string. In zx this will not reliably produce the command stdout (often becomes "[object Object]"), which will break YAML.parse. Use the command output explicitly (e.g., .stdout or .text()) when appending tag contents.
| for (const tag of rewardTags) rawYAML += (await $`git tag -l --format="%(contents)" ${tag}`) + '\n'; | |
| for (const tag of rewardTags) { | |
| const tagContents = await $`git tag -l --format="%(contents)" ${tag}`; | |
| rawYAML += tagContents.stdout + '\n'; | |
| } |
| const PR_DATA = await $`gh api graphql \ | ||
| -f query=${graphqlQuery} \ | ||
| -f owner=${repositoryOwner} \ | ||
| -f name=${repositoryName} \ | ||
| -F number=${issueNumber} \ | ||
| --jq '.data.repository.issue.closedByPullRequestsReferences.nodes[] | select(.merged == true) | {url: .url, mergeCommitSha: .mergeCommit.oid}' | head -n 1`; | ||
|
|
||
| const prData = PR_DATA.text().trim(); | ||
|
|
||
| if (!prData) throw new ReferenceError('No merged PR is found for the given issue number.'); | ||
|
|
||
| const { url: PR_URL, mergeCommitSha } = JSON.parse(prData); |
There was a problem hiding this comment.
gh api ... --jq '...{...}' | head -n 1 is brittle because --jq output for objects is typically multi-line JSON; head -n 1 can truncate it (e.g., just "{") and make JSON.parse(prData) fail. Prefer making the jq output single-line (compact) or extracting a single JSON-encoded line before piping to head, so the parsed string is always valid JSON.
| // Filter out Bot users from the list | ||
| const allUsers = [author.login, ...assignees.map(({ login }) => login)]; | ||
| const users = allUsers.filter(login => !isBotUser(login)); |
There was a problem hiding this comment.
allUsers can contain duplicates (e.g., author is also an assignee), and the current filter doesn’t dedupe. That will incorrectly increase users.length, reduce per-person reward, and may create multiple entries for the same payee. Consider normalizing to unique logins (e.g., via a Set) before calculating the split.
| // Filter out Bot users from the list | |
| const allUsers = [author.login, ...assignees.map(({ login }) => login)]; | |
| const users = allUsers.filter(login => !isBotUser(login)); | |
| // Filter out Bot users from the list and deduplicate logins | |
| const allUsers = [author.login, ...assignees.map(({ login }) => login)]; | |
| const users = [...new Set(allUsers.filter(login => !isBotUser(login)))]; |
| - name: Check for new commits since last statistic | ||
| run: | | ||
| last_tag=$(git describe --tags --abbrev=0 --match "statistic-*" || echo "") | ||
|
|
||
| if [ -z "$last_tag" ]; then | ||
| echo "No previous statistic tags found." | ||
| echo "NEW_COMMITS=true" >> $GITHUB_ENV | ||
| else | ||
| new_commits=$(git log $last_tag..HEAD --oneline) | ||
| if [ -z "$new_commits" ]; then | ||
| echo "No new commits since last statistic tag." | ||
| echo "NEW_COMMITS=false" >> $GITHUB_ENV | ||
| else | ||
| echo "New commits found." |
There was a problem hiding this comment.
The workflow decides whether to run stats based on “new commits since last statistic tag”, but reward tags can be created without any new commits (the claim workflow only pushes tags). In that case this workflow will skip even though there is new reward data. Consider checking for new reward-* tags (or tag dates) instead of commit history, or always run the stats script and let it exit gracefully when there’s no data.
| - name: Check for new commits since last statistic | |
| run: | | |
| last_tag=$(git describe --tags --abbrev=0 --match "statistic-*" || echo "") | |
| if [ -z "$last_tag" ]; then | |
| echo "No previous statistic tags found." | |
| echo "NEW_COMMITS=true" >> $GITHUB_ENV | |
| else | |
| new_commits=$(git log $last_tag..HEAD --oneline) | |
| if [ -z "$new_commits" ]; then | |
| echo "No new commits since last statistic tag." | |
| echo "NEW_COMMITS=false" >> $GITHUB_ENV | |
| else | |
| echo "New commits found." | |
| - name: Check for new reward tags since last statistic | |
| run: | | |
| last_tag=$(git for-each-ref --sort=-creatordate --format='%(refname:short)' "refs/tags/statistic-*" | head -n 1) | |
| if [ -z "$last_tag" ]; then | |
| echo "No previous statistic tags found." | |
| echo "NEW_COMMITS=true" >> $GITHUB_ENV | |
| else | |
| last_tag_ts=$(git for-each-ref --format='%(creatordate:unix)' "refs/tags/$last_tag") | |
| latest_reward_tag_ts=$(git for-each-ref --sort=-creatordate --format='%(creatordate:unix)' "refs/tags/reward-*" | head -n 1) | |
| if [ -z "$latest_reward_tag_ts" ]; then | |
| echo "No reward tags found." | |
| echo "NEW_COMMITS=false" >> $GITHUB_ENV | |
| elif [ "$latest_reward_tag_ts" -le "$last_tag_ts" ]; then | |
| echo "No new reward tags since last statistic tag." | |
| echo "NEW_COMMITS=false" >> $GITHUB_ENV | |
| else | |
| echo "New reward tags found." |
| on: | ||
| push: | ||
| issues: | ||
| pull_request: | ||
| discussion: | ||
| issue_comment: | ||
| discussion_comment: | ||
| pull_request_review_comment: | ||
| release: | ||
| types: | ||
| - published | ||
|
|
||
| jobs: | ||
| send-Lark-message: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v6 | ||
|
|
||
| - uses: denoland/setup-deno@v2 | ||
| with: | ||
| deno-version: v2.x | ||
|
|
||
| - name: Event Message serialization | ||
| id: message | ||
| run: | | ||
| YAML=$( | ||
| cat <<JSON | deno run -A .github/scripts/transform-message.ts | ||
| ${{ toJSON(github) }} | ||
| JSON | ||
| ) | ||
| { | ||
| echo 'content<<EOF' | ||
| echo "$YAML" | ||
| echo 'EOF' | ||
| } >> "$GITHUB_OUTPUT" | ||
|
|
||
| - name: Send message to Lark | ||
| if: ${{ contains(steps.message.outputs.content, ':') }} | ||
| uses: Open-Source-Bazaar/feishu-action@v3 | ||
| with: | ||
| url: ${{ secrets.LARK_CHATBOT_HOOK_URL }} | ||
| msg_type: interactive |
There was a problem hiding this comment.
This workflow runs on pull_request events but the “Send message to Lark” step depends on secrets.LARK_CHATBOT_HOOK_URL, which is not available for PRs from forks; the action may fail or behave unexpectedly. Add a guard to skip the job/step when secrets aren’t available (e.g., only run for non-fork PRs) or adjust the trigger strategy (with appropriate security precautions).
| push: ({ event: { head_commit }, ref, ref_name, server_url, repository, actor }) => { | ||
| const commitUrl = head_commit?.url || `${server_url}/${repository}/tree/${ref_name}`; | ||
| const commitMessage = | ||
| head_commit?.message || 'Create/Delete/Update Branch (No head commit)'; | ||
|
|
||
| return { | ||
| title: 'GitHub 代码提交', | ||
| elements: [ | ||
| { | ||
| tag: 'markdown', | ||
| content: [ | ||
| createContentItem('提交链接:', createLink(commitUrl)), | ||
| createContentItem( | ||
| '代码分支:', | ||
| createLink(`${server_url}/${repository}/tree/${ref_name}`, ref_name) | ||
| ), | ||
| createContentItem( |
There was a problem hiding this comment.
For push events, head_commit.url in the webhook payload is an API URL (not a browser-friendly HTML URL), so the generated “提交链接” may point to the REST endpoint. Consider building the commit page link from server_url, repository, and the commit SHA instead (or otherwise ensure the URL is a web URL).
| { | ||
| "nodeModulesDir": "none" | ||
| } |
There was a problem hiding this comment.
This deno.json lives under .github/scripts/, but the workflows run deno ... .github/scripts/*.ts from the repo root. Deno only auto-loads config from the current directory (and ancestors), so this config likely won’t be applied unless you pass --config .github/scripts/deno.json or move the config to the repository root.
| id: message | ||
| run: | | ||
| YAML=$( | ||
| cat <<JSON | deno run -A .github/scripts/transform-message.ts |
There was a problem hiding this comment.
The serialization step runs deno run -A, which grants full permissions (run, env, net, read/write) even though this script appears to only transform stdin to stdout. Tightening permissions here (remove -A and only grant what’s required) reduces blast radius if the script or its dependencies ever become compromised.
| cat <<JSON | deno run -A .github/scripts/transform-message.ts | |
| cat <<JSON | deno run .github/scripts/transform-message.ts |
Uh oh!
There was an error while loading. Please reload this page.